/** 
 * Allows the valueClasses to be set in MXML, e.g.
 * <signals:Signal id="nameChanged">{[String, uint]}</signals:Signal>
 */
package org.osflash.signals;

import flash.errors.ArgumentError;
import flash.errors.IllegalOperationError;

class OnceSignal implements IOnceSignal {
	public var valueClasses(getValueClasses, setValueClasses) : Array<Dynamic>;
	public var numListeners(getNumListeners, never) : Int;

	var _valueClasses : Array<Dynamic>;
	// of Class
	var slots : SlotList;
	/**
	 * Creates a Signal instance to dispatch value objects.
	 * @param	valueClasses Any number of class references that enable type checks in dispatch().
	 * For example, new Signal([String, Int])
	 * would allow: signal.dispatch(["the Answer", 42])
	 * but not: signal.dispatch([true, 42.5])
	 * nor: signal.dispatch()
	 *
	 */
	public function new(valueClasses:Array<Dynamic>=null) {
		slots = SlotList.NIL;
		if(valueClasses == null)
			valueClasses = [];
		this.valueClasses = valueClasses;
	}

	/**
	 * @inheritDoc
	 * @throws ArgumentError <code>ArgumentError</code>: Invalid valueClasses argument: item at index should be a Class but was not.
	 */
	//@:meta(ArrayElementType(name="Class"))
	public function getValueClasses() : Array<Dynamic> {
		return _valueClasses;
	}

	public function setValueClasses(value : Array<Dynamic>) : Array<Dynamic> {
		// Clone so the Array cannot be affected from outside.
		_valueClasses = (value != null) ? value.slice(0) : [];
		var i : Int = _valueClasses.length;
		while(i-- > 0) {
			if(!(Std.is(_valueClasses[i], Class)))  {
				throw new ArgumentError("Invalid valueClasses argument: " + "item at index " + i + " should be a Class but was:<" + _valueClasses[i] + ">." + HaxeUtil.getQualifiedClassName(_valueClasses[i]));
			}
		}
		return value;
	}

	/** @inheritDoc */
	public function getNumListeners() : Int {
		return slots.length;
	}

	/**
	 * @inheritDoc
	 * @throws flash.errors.IllegalOperationError <code>IllegalOperationError</code>: You cannot addOnce() then add() the same listener without removing the relationship first.
	 * @throws ArgumentError <code>ArgumentError</code>: Given listener is <code>null</code>.
	 */
	public function addOnce(listener : Dynamic) : ISlot {
		return registerListener(listener, true);
	}

	/** @inheritDoc */
	public function remove(listener : Dynamic) : ISlot {
		var slot : ISlot = slots.find(listener);
		if(slot == null) return null;
		slots = slots.filterNot(listener);
		return slot;
	}

	/** @inheritDoc */
	public function removeAll() : Void {
		slots = SlotList.NIL;
	}

	/**
	 * @inheritDoc
	 * @throws ArgumentError <code>ArgumentError</code>: Incorrect number of arguments.
	 * @throws ArgumentError <code>ArgumentError</code>: Value object is not an instance of the appropriate valueClasses Class.
	 */
	public function dispatch(valueObjects:Array<Dynamic>=null) : Void {
		if(valueObjects == null)
			valueObjects = [];
		// If valueClasses is empty, value objects are not type-checked.
		var numValueClasses : Int = _valueClasses.length;
		var numValueObjects : Int = valueObjects.length;
		// Cannot dispatch fewer objects than declared classes.
		if(numValueObjects < numValueClasses)  {
			throw new ArgumentError("Incorrect number of arguments. " + "Expected at least " + numValueClasses + " but received " + numValueObjects + ".");
		}
		var i : Int = 0;
		while(i < numValueClasses) {
			// Optimized for the optimistic case that values are correct.
			if(Std.is(valueObjects[i], _valueClasses[i]) || valueObjects[i] == null)  {
				i++;
				continue;
			}
			throw new ArgumentError("Value object <" + valueObjects[i] + "> is not an instance of <" + _valueClasses[i] + ">.");
			i++;
		}
		// Broadcast to listeners.
		var slotsToProcess : SlotList = slots;
		if(slotsToProcess.nonEmpty)  {
			while(slotsToProcess.nonEmpty) {
				slotsToProcess.head.execute(valueObjects);
				slotsToProcess = slotsToProcess.tail;
			}

		}
	}

	function registerListener(listener : Dynamic, once : Bool = false) : ISlot {
		if(registrationPossible(listener, once))  {
			var newSlot : ISlot = new Slot(listener, this, once);
			slots = slots.prepend(newSlot);
			return newSlot;
		}
		return slots.find(listener);
	}

	function registrationPossible(listener : Dynamic, once : Bool) : Bool {
		if(!slots.nonEmpty)
			return true;
		var existingSlot : ISlot = slots.find(listener);
		if(existingSlot == null)
			return true;
		if(existingSlot.once != once)  {
			// If the listener was previously added, definitely don't add it again.
			// But throw an exception if their once values differ.
			throw new IllegalOperationError("You cannot addOnce() then add() the same listener without removing the relationship first.");
		}
		return false;
		// Listener was already registered.
	}

}

